package com.example.democlient.network;


import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;

import com.example.democlient.network.timeout.ReaderIdleTimeoutTask;
import com.example.democlient.network.timeout.WriterIdleTimeoutTask;

public class Dispatcher extends Thread {

    /**
     * Selector thats selecting ready keys.
     */
    Selector selector;
    /**
     * Object on witch register vs selector.select are synchronized
     */
    private final Object gate = new Object();

    /**
     * Constructor.
     *
     * @throws IOException
     */
    public Dispatcher() throws IOException {
        this.selector = SelectorProvider.provider().openSelector();
        System.out.println("selector="+selector.isOpen());
    }

    /**
     * 
     * @throws IOException
     */
	public void dispatch() throws IOException{
        int selected = selector.select();
        if (selected != 0) {
            Iterator<SelectionKey> selectedKeys = this.selector.selectedKeys().iterator();
            while (selectedKeys.hasNext()) {
                SelectionKey key = selectedKeys.next();
                selectedKeys.remove();
                if (!key.isValid())
                    continue;
                switch (key.readyOps()) {
                    case SelectionKey.OP_READ:
                        this.read(key);
                        break;
                    case SelectionKey.OP_WRITE:
                        this.write(key);
                        break;
                }
            }
        }
	}

    /**
     * @return Selector of this Dispatcher
     */
    public final Selector selector() {
        return this.selector;
    }

    /**
     * Dispatching Selected keys and processing pending close.
     *
     * @see java.lang.Thread#run()
     */
    @Override
    public void run() {
        for (; ;) {
            try {
                dispatch();
                synchronized (gate) {
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Register new client connected to this Dispatcher and set SelectionKey (result of registration) as this key of
     * given AConnection.
     *
     * @param ch
     * @param ops
     * @param att
     * @throws IOException
     */
    public final void register(SelectableChannel ch, int ops, NetConnection att) throws IOException {
        synchronized (gate) {
            selector.wakeup();
            //三个参数需要知道
            att.setKey(ch.register(selector, ops, att));
        }
    }


    /**
     * Read data from socketChannel represented by SelectionKey key. Parse and Process data. Prepare buffer for next
     * read.
     *
     * @param key
     */
    final void read(SelectionKey key) {
    	ReaderIdleTimeoutTask.lastReadTime = System.currentTimeMillis();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        NetConnection con = (NetConnection) key.attachment();

        ByteBuffer rb = con.readBuffer;

        int numRead;
        try {
            numRead = socketChannel.read(rb);
        }
        catch (IOException e) {
            closeConnectionImpl(con);
            return;
        }

        if (numRead == -1) {
            closeConnectionImpl(con);
            return;
        } else if (numRead == 0) {
            return;
        }

        rb.flip();
        while (rb.remaining() > 2 && rb.remaining() >= rb.getShort(rb.position())) {
            if (!parse(con, rb)) {
                closeConnectionImpl(con);
                return;
            }
        }
        if (rb.hasRemaining()) {
            con.readBuffer.compact();
        } else
            rb.clear();
    }

    /**
     * Parse data from buffer and prepare buffer for reading just one packet - call processData(ByteBuffer b).
     *
     * @param con Connection
     * @param buf Buffer with packet data
     * @return True if packet was parsed.
     */
    private boolean parse(NetConnection con, ByteBuffer buf) {
        short sz = 0;
        try {
            sz = buf.getShort();
            if (sz > 1)
                sz -= 2;
            ByteBuffer b = (ByteBuffer) buf.slice().limit(sz);
            buf.position(buf.position() + sz);
            return con.processData(b);
        }
        catch (IllegalArgumentException e) {
            return false;
        }
    }

    /**
     * Write as much as possible data to socketChannel represented by SelectionKey key. If all data were written key
     * write interest will be disabled.
     *
     * @param key
     */
    final void write(SelectionKey key) {
    	WriterIdleTimeoutTask.lastWriteTime = System.currentTimeMillis();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        NetConnection con = (NetConnection) key.attachment();

        int numWrite;
        ByteBuffer wb = con.writeBuffer;
        if (wb.hasRemaining()) {
            try {
                numWrite = socketChannel.write(wb);
            }
            catch (IOException e) {
                closeConnectionImpl(con);
                return;
            }

            if (numWrite == 0) {
                return;
            }
            if (wb.hasRemaining())
                return;
        }

        while (true) {
            wb.clear();
            boolean writeFailed = !con.writeData(wb);

            if (writeFailed) {
                wb.limit(0);
                break;
            }

            try {
                numWrite = socketChannel.write(wb);
            }
            catch (IOException e) {
                closeConnectionImpl(con);
                return;
            }

            if (numWrite == 0) {
                return;
            }

            /** not all data was send */
            if (wb.hasRemaining())
                return;
        }
        /**
         * We wrote away all data, so we're no longer interested in writing on this socket.
         */
        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);

        /** We wrote all data so we can close connection that is "PandingClose" */
        if (con.isPendingClose())
            closeConnectionImpl(con);
    }

    /**
     * Connection will be closed [onlyClose()] and onDisconnect() method will be executed on another thread
     * [DisconnectionThreadPool] after getDisconnectionDelay() time in ms. This method may only be called by current
     * Dispatcher Thread.
     *
     * @param con
     */
    protected final void closeConnectionImpl(NetConnection con) {
    	SocketService.getInstance().restart();
    }
}
